import { Application, NextFunction, Request, Response } from 'express';
import fs from 'fs';
import passport from 'passport';
import { SamlConfig, Strategy as SamlStrategy } from 'passport-saml';
import {
  getNewComerFromDocument,
  UserDocument,
  UserLoginStatus,
  UserRole,
  UserSessionInfo,
} from '../model/user';
import { validateUser } from '../routes/user';
import { AsyncError, fetchUserByLoginId } from '../util/asyncUtils';

const dev = process.env.NODE_ENV !== 'production';
const isStaging = process.env.APP_ENV === 'staging';
/**
 * SSO (Single Sign-On) Test using ADFS (Active Drectory Federation Service):
 * Protocol: SAML 2.0
 * Allowed SP for Test: https://localhost:44364
 */
const myAddress = dev
  ? 'dir.bigdata.samsung.com.cn'
  : isStaging
  // ? 'dir-stg-portals.samsungdiroute.net'
  ? 'dir.bigdata.samsung.com.cn'
  : 'dir.bigdata.samsung.com.cn';
// const myPort = !dev || isStaging ? 443 : 443;
const myPort = 443
const credDirPath = dev ? './server/creds' : './dist/server/creds';
const SpMeta = {
  address: `https://${myAddress}:${myPort}`,
  port: myPort,
  httpsOptions:
    dev && !isStaging
      ? {
          key: fs.readFileSync(`${credDirPath}/localhost.key`),
          cert: fs.readFileSync(`${credDirPath}/localhost.crt`), // Self-Signed Cert (X.509)
        }
      : {},
  loginCallbackUri: '/login/callback',
  logoutCallbackUri: '/logout/callback',
};
export const AuthConf = SpMeta;

const IdpMeta = {
  // address: dev || isStaging ? 'https://sts-dev.secsso.net' : 'https://sts.secsso.net',
  address: dev || isStaging ? 'https://sts.secsso.net' : 'https://sts.secsso.net',
  cert: fs.readFileSync(
    dev || isStaging
      // ? `${credDirPath}/sts-dev.secsso.net.pem`
      ? `${credDirPath}/sts.secsso.net.pem`
      : `${credDirPath}/sts.secsso.net.pem`,
    'UTF8',
  ),
};

/**
 * SAML 2.0 Metadata ensures a secure transaction
 * between an IdP (Identity Provider) and SP (Service Provider)
 *
 * For more details, please visit :
 * - https://en.wikipedia.org/wiki/SAML_2.0#SAML_2.0_metadata
 * - https://github.com/node-saml/passport-saml#config-parameter-details
 */
const SamlMeta: SamlConfig = {
  entryPoint: `${IdpMeta.address}/adfs/ls`,
  logoutUrl: `${IdpMeta.address}/adfs/ls/?wa=wsignoutcleanup1.0`,
  callbackUrl: `${SpMeta.address + SpMeta.loginCallbackUri}`,
  logoutCallbackUrl: `${SpMeta.address + SpMeta.logoutCallbackUri}`,
  issuer: SpMeta.address,
  cert: IdpMeta.cert,
  identifierFormat: null,
  validateInResponseTo: false,
  acceptedClockSkewMs: -1, // SAML assertion not yet valid; SAML.checkTimestampsValidityError;
  passReqToCallback: true, // makes req available in callback
};

const samlAttrNamePrefix = 'http://schemas.sec.com/2018/05/identity/claims';

export const createAuthMiddleware = (server: Application) => {
  const authName = 'ad-sso';
  let userInfo: UserSessionInfo;
  /**
   * Configure Auth Strategy.
   *
   * For more details, please visit:
   *  - https://github.com/node-saml/passport-saml/blob/master/src/strategy.ts
   */
  passport.use(
    authName,
    new SamlStrategy(SamlMeta, async (req, profile, done) => {
      console.info(`-SamlStrategy-`);
      if (profile) {
	      
        console.info(`-valid Profile-`);
        console.info(`LoginId: ${samlAttrNamePrefix}/LoginId`);
        console.info(`Username: ${samlAttrNamePrefix}/Username`);
        console.info(`DeptName: ${samlAttrNamePrefix}/DeptName`);
        // valid Profile, parse user info from SAML Assertion.
        const userDocument: UserDocument = {
          inResponseTo: profile.ID, // ID of samlp:AuthnRequest
          loginId: profile[`${samlAttrNamePrefix}/LoginId`] as string,
          email: profile[`${samlAttrNamePrefix}/Mail`] as string,
          nameKr: profile[`${samlAttrNamePrefix}/Username`] as string,
          nameEn: profile[`${samlAttrNamePrefix}/UserName_EN`] as string,
          groupNameKr: profile[`${samlAttrNamePrefix}/DeptName`] as string,
          groupNameEn: profile[`${samlAttrNamePrefix}/DeptName_EN`] as string,
          departmentId: profile[`${samlAttrNamePrefix}/DeptId`] as string,
          // departmentId: profile[`${samlAttrNamePrefix}/DeptNumber`] as string, //原
        };

        // verify whether entrolled user or not.
        try {
          userInfo = await fetchUserByLoginId(userDocument.loginId as string);
          // append user document for revocation.
          userInfo.adfs = userDocument;
          done(null, userInfo);
        } catch (error) {
          const err: AsyncError = error;
          console.error(err.message);
          /**
           * [WARN] Process for new user
           * It is required to maintain temporal session until authorization is complete.
           * otherwise, delegate to another strategy by calling done(null, undefined).
           * it will be resulting in strategy.fail(info, 401);
           */
          if (error.response && error.response.status === 404) {
            userInfo = getNewComerFromDocument(userDocument);
            done(null, userInfo);
          } else {
            done(null, undefined);
          }
        }
      } else {
        console.info(`-invalid Profile-undefined`);
        // in case of 'null | undefined'
        done(null, undefined);
      }
    }),
  );

  passport.serializeUser((user, done) => {
    done(null, user);
  });

  passport.deserializeUser((id, done) => {
    const user = id as UserSessionInfo;
    done(null, user);
  });

  // initialize strategy and session.
  server.use(passport.initialize());
  server.use(passport.session());

  /**
   * Register authentication callback
   *
   * For more details, please visit:
   *  - https://github.com/jaredhanson/passport/blob/master/lib/middleware/authenticate.js
   */
  server.post(
    `${SpMeta.loginCallbackUri}`,
    (req: Request, res: Response, next: NextFunction) => {
      console.info('Verify samlp:Response from IdP.');
      passport.authenticate(authName, {
        successReturnToOrRedirect: '/login/authorize',
        failureRedirect: '/login/sso', // authentication failure, invoked when all strategies are failed
        failureFlash: false,
        passReqToCallback: true,
      })(req, res, next);
    },
  );

  // Register handler for authenticate request
  server.get('/login/sso', (req: Request, res: Response, next: NextFunction) => {
    console.info('Send samlp:AuthnRequest to IdP.');
    passport.authenticate(authName)(req, res, next);
  });

  // Register handler for authorize request
  server.get(
    '/login/authorize',
    async (req: Request, res: Response, next: NextFunction) => {
      console.log('~~~ authorized, req.user:',req.user)
 
      const user = req.user as UserSessionInfo;
      if (user.status.loginStatus === UserLoginStatus.Approved) {
        // check whether login user still valid or not.
	console.log('~~~ authorized, 222 ')
        const tempUser = await validateUser(user);
        if (
          tempUser.status.loginStatus === UserLoginStatus.Inactive ||
          tempUser.status.loginStatus === UserLoginStatus.Revoked
        ) {
          // update current session with the given user info.
          //@ts-ignore
          req._passport.session.user = tempUser;
          req.session.save(err => {
            res.redirect('/myInfo');
          });
        } else {
          res.redirect('/');
        }
      } else {
        // redirect to registration page.
        res.redirect('/myInfo');
      }
    },
  );
};

const RootPages = ['/', '/index', '/profile', '/notice'];
const SubPages = ['/activeness/', '/onboarding/', '/notice/'];

export const isWebRequest = (req: Request, blockAll?: boolean): boolean => {
  if (blockAll && req.path.match('/myInfo')) {
    return true;
  }
  if (RootPages.includes(req.path)) {
    // console.info(`Exact matched path: ${req.path}`);
    return true;
  } else {
    /**
     * If sub-pages return true, otherwise false including Ajax calls.
     */
    return SubPages.filter(p => {
      const array = req.path.match(p);
      if (array) {
        // console.info(`Regex matched path: ${req.path}`);
        return array;
      }
    }).length > 0
      ? true
      : false;
  }
};

/**
 * Login Required middleware.
 */
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
  const local = process.env.APP_ENV === 'localhost';

  console.info(`User(${req.ip}) is NOT authenticated for ${req.originalUrl}`);
  console.info(`User(${req.user}) is user`);
  console.info(`User(${local}) is local`);
  console.info(`User(${process.env.APP_ENV}) is app env`);

  if (local && req.app.locals.name !== 'auth-test') {
    return next();
  }
  if (process.env.APP_ENV === 'production') {
    //return next();
  }
  if (req.isAuthenticated()) {
    const user = req.user as UserSessionInfo;
    if (user.uid) {
      // valid user
      return next();
    } else if (user.status.loginStatus === UserLoginStatus.NewComer) {
      // [Warn] Exceptional case for user registration.
      // Using exact match, redirect to registratin page if user doesn't complete registration process.
      return isWebRequest(req) ? res.redirect('/myInfo') : next();
    }
  }
  console.info(`User(${req.ip}) is NOT -authenticated for ${req.originalUrl}`);
  res.redirect('/login/sso');
};

// RBAC
export const isWebAuthorized = (req: Request, res: Response, next: NextFunction) => {
  const user = req.user as UserSessionInfo;
  if (
    !req.path.match('/myInfo') &&
    user.status.loginStatus !== UserLoginStatus.Approved
  ) {
    return isWebRequest(req) ? res.redirect('/myInfo') : next();
  }
  return next();
};

export const isAuthorized = (req: Request, res: Response, next: NextFunction) => {
  const user = req.user as UserSessionInfo;
  const isNotAuthorized =
    user?.status.loginStatus === UserLoginStatus.NewComer ||
    user?.status.loginStatus === UserLoginStatus.Inactive ||
    (user?.info.role === UserRole.Customer &&
      user?.status.loginStatus === UserLoginStatus.Pending);

  if (isNotAuthorized) {
    return res.status(403).json({
      reason: '[DIR Beta] You are NOT authorized for requested data.',
    });
  } else return next();
};
